/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.solr.handler.export;

import com.fasterxml.jackson.databind.ObjectMapper;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import org.apache.lucene.tests.util.TestUtil;
import org.apache.solr.SolrTestCaseJ4;
import org.apache.solr.common.SolrInputDocument;
import org.apache.solr.common.params.StreamParams;
import org.apache.solr.common.util.SuppressForbidden;
import org.apache.solr.common.util.Utils;
import org.apache.solr.index.LogDocMergePolicyFactory;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.schema.SchemaField;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;

public class TestExportWriter extends SolrTestCaseJ4 {

  private ObjectMapper mapper = new ObjectMapper();

  @BeforeClass
  public static void beforeClass() throws Exception {
    // force LogDocMergePolicy so that we get a predictable doc order
    // when testing index order results
    systemSetPropertySolrTestsMergePolicyFactory(LogDocMergePolicyFactory.class.getName());
    initCore("solrconfig-sortingresponse.xml", "schema-sortingresponse.xml");
  }

  @Before
  @Override
  public void setUp() throws Exception {
    super.setUp();
    assertU(delQ("*:*"));
    assertU(commit());
  }

  @Test
  public void testEmptyValues() throws Exception {
    // Index 2 document with one document that doesn't have field2_i_p
    // Sort and return field2_i_p
    // Test SOLR-12572 for potential NPEs
    assertU(delQ("*:*"));
    assertU(commit());

    assertU(adoc("id", "1", "field2_i_p", "1"));
    assertU(adoc("id", "2"));
    assertU(commit());

    String resp =
        h.query(req("q", "*:*", "qt", "/export", "fl", "id,field2_i_p", "sort", "field2_i_p asc"));
    assertJsonEquals(
        resp,
        "{\n"
            + "  \"responseHeader\":{\"status\":0},\n"
            + "  \"response\":{\n"
            + "    \"numFound\":2,\n"
            + "    \"docs\":[{\n"
            + "        \"id\":\"2\"}\n"
            + "      ,{\n"
            + "        \"id\":\"1\",\n"
            + "        \"field2_i_p\":1}]}}");
  }

  @Test
  public void testSortingOnFieldWithNoValues() throws Exception {
    assertU(delQ("*:*"));
    assertU(commit());

    assertU(adoc("id", "1"));
    assertU(commit());

    // 10 fields
    List<String> fieldNames =
        new ArrayList<>(
            Arrays.asList(
                "floatdv",
                "intdv",
                "stringdv",
                "longdv",
                "doubledv",
                "datedv",
                "booleandv",
                "field1_s_dv",
                "field2_i_p",
                "field3_l_p"));
    for (String sortField : fieldNames) {
      String resp =
          h.query(
              req(
                  "q",
                  "*:*",
                  "qt",
                  "/export",
                  "fl",
                  "id," + sortField,
                  "sort",
                  sortField + " desc"));
      assertJsonEquals(
          resp,
          "{\n"
              + "  \"responseHeader\":{\"status\":0},\n"
              + "  \"response\":{\n"
              + "    \"numFound\":1,\n"
              + "    \"docs\":[{\n"
              + "        \"id\":\"1\"}]}}");
    }
  }

  public static void createIndex() {
    assertU(
        adoc(
            "id", "1",
            "floatdv", "2.1",
            "intdv", "1",
            "stringdv", "hello world",
            "longdv", "323223232323",
            "doubledv", "2344.345",
            "intdv_m", "100",
            "intdv_m", "250",
            "floatdv_m", "123.321",
            "floatdv_m", "345.123",
            "doubledv_m", "3444.222",
            "doubledv_m", "23232.2",
            "longdv_m", "43434343434",
            "longdv_m", "343332",
            "stringdv_m", "manchester \"city\"",
            "stringdv_m", "liverpool",
            "stringdv_m", "Everton",
            "datedv", "2017-06-16T07:00:00Z",
            "datedv_m", "2017-06-16T01:00:00Z",
            "datedv_m", "2017-06-16T02:00:00Z",
            "datedv_m", "2017-06-16T03:00:00Z",
            "datedv_m", "2017-06-16T04:00:00Z",
            "sortabledv_m", "this is some text one_1",
            "sortabledv_m", "this is some text two_1",
            "sortabledv_m", "this is some text three_1",
            "sortabledv_m_udvas", "this is some text one_1",
            "sortabledv_m_udvas", "this is some text two_1",
            "sortabledv_m_udvas", "this is some text three_1"));

    assertU(
        adoc(
            "id",
            "7",
            "floatdv",
            "2.1",
            "intdv",
            "7",
            "longdv",
            "323223232323",
            "doubledv",
            "2344.345",
            "floatdv_m",
            "123.321",
            "floatdv_m",
            "345.123",
            "doubledv_m",
            "3444.222",
            "doubledv_m",
            "23232.2",
            "longdv_m",
            "43434343434",
            "longdv_m",
            "343332"));

    assertU(commit());
    assertU(
        adoc(
            "id",
            "2",
            "floatdv",
            "2.1",
            "intdv",
            "2",
            "stringdv",
            "hello world",
            "longdv",
            "323223232323",
            "doubledv",
            "2344.344"));
    assertU(commit());
    assertU(
        adoc(
            "id",
            "3",
            "floatdv",
            "2.1",
            "intdv",
            "3",
            "stringdv",
            "chello world",
            "longdv",
            "323223232323",
            "doubledv",
            "2344.346",
            "intdv_m",
            "100",
            "intdv_m",
            "250",
            "floatdv_m",
            "123.321",
            "floatdv_m",
            "345.123",
            "doubledv_m",
            "3444.222",
            "doubledv_m",
            "23232.2",
            "longdv_m",
            "43434343434",
            "longdv_m",
            "343332",
            "stringdv_m",
            "manchester \"city\"",
            "stringdv_m",
            "liverpool",
            "stringdv_m",
            "everton",
            "int_is_t",
            "1",
            "int_is_t",
            "1",
            "int_is_t",
            "1",
            "int_is_t",
            "1",
            "sortabledv",
            "this is some text_1",
            "sortabledv_udvas",
            "this is some text_1"));
    assertU(commit());
    assertU(
        adoc(
            "id",
            "8",
            "floatdv",
            "2.1",
            "intdv",
            "10000000",
            "stringdv",
            "chello \"world\"",
            "longdv",
            "323223232323",
            "doubledv",
            "2344.346",
            "intdv_m",
            "100",
            "intdv_m",
            "250",
            "floatdv_m",
            "123.321",
            "floatdv_m",
            "345.123",
            "doubledv_m",
            "3444.222",
            "doubledv_m",
            "23232.2",
            "longdv_m",
            "43434343434",
            "longdv_m",
            "343332",
            "stringdv_m",
            "manchester \"city\"",
            "stringdv_m",
            "liverpool",
            "stringdv_m",
            "everton",
            "datedv",
            "2017-01-01T00:00:00Z",
            "datedv_m",
            "2017-01-01T01:00:00Z",
            "datedv_m",
            "2017-01-01T02:00:00Z",
            "int_is_p",
            "1",
            "int_is_p",
            "1",
            "int_is_p",
            "1",
            "int_is_p",
            "1",
            "sortabledv",
            "this is some text_2",
            "sortabledv_udvas",
            "this is some text_2",
            "sortabledv_m",
            "this is some text one_2",
            "sortabledv_m",
            "this is some text two_2",
            "sortabledv_m",
            "this is some text three_2",
            "sortabledv_m_udvas",
            "this is some text one_2",
            "sortabledv_m_udvas",
            "this is some text two_2",
            "sortabledv_m_udvas",
            "this is some text three_2"));
    assertU(commit());
  }

  @Test
  public void test() throws Exception {
    clearIndex();
    createIndex();

    testSortingOutput();
    testExportRequiredParams();
    testDates();
    testDuplicates();

    clearIndex();
  }

  @Test
  public void testSmallChains() throws Exception {
    clearIndex();

    assertU(adoc("id", "1", "field1_i_p", Integer.toString(Integer.MIN_VALUE), "field2_i_p", "1"));
    assertU(commit());

    assertU(
        adoc(
            "id",
            "2",
            "field1_i_p",
            Integer.toString(Integer.MIN_VALUE),
            "field2_i_p",
            Integer.toString(Integer.MIN_VALUE + 1)));
    assertU(commit());

    assertU(
        adoc(
            "id",
            "3",
            "field1_i_p",
            Integer.toString(Integer.MIN_VALUE),
            "field2_i_p",
            Integer.toString(Integer.MIN_VALUE)));
    assertU(commit());

    // Test single value DocValue output
    // Expected for asc sort doc3 -> doc2 -> doc1
    String s =
        h.query(
            req("q", "*:*", "qt", "/export", "fl", "id", "sort", "field1_i_p asc,field2_i_p asc"));
    assertJsonEquals(
        s,
        "{\n"
            + "  \"responseHeader\":{\"status\":0},\n"
            + "  \"response\":{\n"
            + "    \"numFound\":3,\n"
            + "    \"docs\":[{\n"
            + "        \"id\":\"3\"}\n"
            + "      ,{\n"
            + "        \"id\":\"2\"}\n"
            + "      ,{\n"
            + "        \"id\":\"1\"}]}}");

    clearIndex();
    // Adding 3 docs of integers with the following values
    //  doc1: Integer.MIN_VALUE,1,2,Integer.MAX_VALUE,3,4,5,6
    //  doc2: Integer.MIN_VALUE,Integer.MIN_VALUE,2,Integer.MAX_VALUE,4,4,5,6
    //  doc3: Integer.MIN_VALUE,Integer.MIN_VALUE,2,Integer.MAX_VALUE,3,4,5,6

    assertU(
        adoc(
            "id",
            "1",
            "field1_i_p",
            Integer.toString(Integer.MIN_VALUE),
            "field2_i_p",
            "1",
            "field3_i_p",
            "2",
            "field4_i_p",
            Integer.toString(Integer.MAX_VALUE),
            "field5_i_p",
            "3",
            "field6_i_p",
            "4",
            "field7_i_p",
            "5",
            "field8_i_p",
            "6"));
    assertU(commit());

    assertU(
        adoc(
            "id",
            "2",
            "field1_i_p",
            Integer.toString(Integer.MIN_VALUE),
            "field2_i_p",
            Integer.toString(Integer.MIN_VALUE),
            "field3_i_p",
            "2",
            "field4_i_p",
            Integer.toString(Integer.MAX_VALUE),
            "field5_i_p",
            "4",
            "field6_i_p",
            "4",
            "field7_i_p",
            "5",
            "field8_i_p",
            "6"));
    assertU(commit());

    assertU(
        adoc(
            "id",
            "3",
            "field1_i_p",
            Integer.toString(Integer.MIN_VALUE),
            "field2_i_p",
            Integer.toString(Integer.MIN_VALUE),
            "field3_i_p",
            "2",
            "field4_i_p",
            Integer.toString(Integer.MAX_VALUE),
            "field5_i_p",
            "3",
            "field6_i_p",
            "4",
            "field7_i_p",
            "5",
            "field8_i_p",
            "6"));
    assertU(commit());

    s =
        h.query(
            req(
                "q",
                "*:*",
                "qt",
                "/export",
                "fl",
                "id",
                "sort",
                "field1_i_p asc,field2_i_p asc,field3_i_p asc,field4_i_p asc,field5_i_p desc,field6_i_p desc,field7_i_p desc,field8_i_p asc"));
    assertJsonEquals(
        s,
        "{\n"
            + "  \"responseHeader\":{\"status\":0},\n"
            + "  \"response\":{\n"
            + "    \"numFound\":3,\n"
            + "    \"docs\":[{\n"
            + "        \"id\":\"2\"}\n"
            + "      ,{\n"
            + "        \"id\":\"3\"}\n"
            + "      ,{\n"
            + "        \"id\":\"1\"}]}}");
  }

  @Test
  public void testIndexOrder() throws Exception {
    clearIndex();

    assertU(adoc("id", "1", "stringdv", "a"));
    assertU(adoc("id", "2", "stringdv", "a"));

    assertU(commit());

    assertU(adoc("id", "3", "stringdv", "a"));
    assertU(adoc("id", "4", "stringdv", "a"));

    assertU(commit());

    String expectedResult =
        "{\n"
            + "  \"responseHeader\":{\"status\":0},\n"
            + "  \"response\":{\n"
            + "    \"numFound\":4,\n"
            + "    \"docs\":[{\n"
            + "        \"id\":\"1\"}\n"
            + "      ,{\n"
            + "        \"id\":\"2\"}\n"
            + "      ,{\n"
            + "        \"id\":\"3\"}\n"
            + "      ,{\n"
            + "        \"id\":\"4\"}]}}";

    String s = h.query(req("q", "*:*", "qt", "/export", "fl", "id", "sort", "stringdv asc"));
    assertJsonEquals(s, expectedResult);

    s = h.query(req("q", "*:*", "qt", "/export", "fl", "id", "sort", "stringdv desc"));
    assertJsonEquals(s, expectedResult);
  }

  @Test
  public void testStringWithCase() throws Exception {
    clearIndex();

    assertU(adoc("id", "1", "stringdv", "a"));
    assertU(adoc("id", "2", "stringdv", "ABC"));

    assertU(commit());

    assertU(adoc("id", "3", "stringdv", "xyz"));
    assertU(adoc("id", "4", "stringdv", "a"));

    assertU(commit());

    String s = h.query(req("q", "*:*", "qt", "/export", "fl", "id", "sort", "stringdv desc"));
    assertJsonEquals(
        s,
        "{\n"
            + "  \"responseHeader\":{\"status\":0},\n"
            + "  \"response\":{\n"
            + "    \"numFound\":4,\n"
            + "    \"docs\":[{\n"
            + "        \"id\":\"3\"}\n"
            + "      ,{\n"
            + "        \"id\":\"1\"}\n"
            + "      ,{\n"
            + "        \"id\":\"4\"}\n"
            + "      ,{\n"
            + "        \"id\":\"2\"}]}}");
  }

  @Test
  public void testBooleanField() throws Exception {
    clearIndex();

    assertU(adoc("id", "1", "booleandv", "true"));
    assertU(commit());

    assertU(adoc("id", "2", "booleandv", "false"));
    assertU(commit());

    String s = h.query(req("q", "*:*", "qt", "/export", "fl", "id", "sort", "booleandv asc"));
    assertJsonEquals(
        s,
        "{\n"
            + "  \"responseHeader\":{\"status\":0},\n"
            + "  \"response\":{\n"
            + "    \"numFound\":2,\n"
            + "    \"docs\":[{\n"
            + "        \"id\":\"2\"}\n"
            + "      ,{\n"
            + "        \"id\":\"1\"}]}}");

    s = h.query(req("q", "*:*", "qt", "/export", "fl", "id", "sort", "booleandv desc"));
    assertJsonEquals(
        s,
        "{\n"
            + "  \"responseHeader\":{\"status\":0},\n"
            + "  \"response\":{\n"
            + "    \"numFound\":2,\n"
            + "    \"docs\":[{\n"
            + "        \"id\":\"1\"}\n"
            + "      ,{\n"
            + "        \"id\":\"2\"}]}}");
  }

  private void testSortingOutput() throws Exception {

    // Test single value DocValue output
    String s =
        h.query(
            req(
                "q",
                "id:1",
                "qt",
                "/export",
                "fl",
                "floatdv,intdv,stringdv,longdv,doubledv",
                "sort",
                "intdv asc"));

    assertJsonEquals(
        s,
        "{\"responseHeader\": {\"status\": 0}, \"response\":{\"numFound\":1, \"docs\":[{\"floatdv\":2.1,\"intdv\":1,\"stringdv\":\"hello world\",\"longdv\":323223232323,\"doubledv\":2344.345}]}}");

    // Test null value string:
    s =
        h.query(
            req(
                "q",
                "id:7",
                "qt",
                "/export",
                "fl",
                "floatdv,intdv,stringdv,longdv,doubledv",
                "sort",
                "intdv asc"));

    assertJsonEquals(
        s,
        "{\"responseHeader\": {\"status\": 0}, \"response\":{\"numFound\":1, \"docs\":[{\"floatdv\":2.1,\"intdv\":7,\"longdv\":323223232323,\"doubledv\":2344.345}]}}");

    // Test multiValue docValues output
    s =
        h.query(
            req(
                "q",
                "id:1",
                "qt",
                "/export",
                "fl",
                "intdv_m,floatdv_m,doubledv_m,longdv_m,stringdv_m",
                "sort",
                "intdv asc"));
    assertJsonEquals(
        s,
        "{\"responseHeader\": {\"status\": 0}, \"response\":{\"numFound\":1, \"docs\":[{\"intdv_m\":[100,250],\"floatdv_m\":[123.321,345.123],\"doubledv_m\":[3444.222,23232.2],\"longdv_m\":[343332,43434343434],\"stringdv_m\":[\"Everton\",\"liverpool\",\"manchester \\\"city\\\"\"]}]}}");

    // Test multiValues docValues output with nulls
    s =
        h.query(
            req(
                "q",
                "id:7",
                "qt",
                "/export",
                "fl",
                "intdv_m,floatdv_m,doubledv_m,longdv_m,stringdv_m",
                "sort",
                "intdv asc"));
    assertJsonEquals(
        s,
        "{\"responseHeader\": {\"status\": 0}, \"response\":{\"numFound\":1, \"docs\":[{\"floatdv_m\":[123.321,345.123],\"doubledv_m\":[3444.222,23232.2],\"longdv_m\":[343332,43434343434]}]}}");

    // Test single sort param is working
    s = h.query(req("q", "id:(1 2)", "qt", "/export", "fl", "intdv", "sort", "intdv desc"));
    assertJsonEquals(
        s,
        "{\"responseHeader\": {\"status\": 0}, \"response\":{\"numFound\":2, \"docs\":[{\"intdv\":2},{\"intdv\":1}]}}");

    s = h.query(req("q", "id:(1 2)", "qt", "/export", "fl", "intdv", "sort", "intdv asc"));
    assertJsonEquals(
        s,
        "{\"responseHeader\": {\"status\": 0}, \"response\":{\"numFound\":2, \"docs\":[{\"intdv\":1},{\"intdv\":2}]}}");

    // Test sort on String will null value. Null value should sort last on desc and first on asc.

    s = h.query(req("q", "id:(1 7)", "qt", "/export", "fl", "intdv", "sort", "stringdv desc"));
    assertJsonEquals(
        s,
        "{\"responseHeader\": {\"status\": 0}, \"response\":{\"numFound\":2, \"docs\":[{\"intdv\":1},{\"intdv\":7}]}}");

    s = h.query(req("q", "id:(1 7)", "qt", "/export", "fl", "intdv", "sort", "stringdv asc"));
    assertJsonEquals(
        s,
        "{\"responseHeader\": {\"status\": 0}, \"response\":{\"numFound\":2, \"docs\":[{\"intdv\":7},{\"intdv\":1}]}}");

    // Test multi-sort params
    s =
        h.query(
            req("q", "id:(1 2)", "qt", "/export", "fl", "intdv", "sort", "floatdv asc,intdv desc"));
    assertJsonEquals(
        s,
        "{\"responseHeader\": {\"status\": 0}, \"response\":{\"numFound\":2, \"docs\":[{\"intdv\":2},{\"intdv\":1}]}}");

    s =
        h.query(
            req("q", "id:(1 2)", "qt", "/export", "fl", "intdv", "sort", "floatdv desc,intdv asc"));
    assertJsonEquals(
        s,
        "{\"responseHeader\": {\"status\": 0}, \"response\":{\"numFound\":2, \"docs\":[{\"intdv\":1},{\"intdv\":2}]}}");

    // Test three sort fields
    s =
        h.query(
            req(
                "q",
                "id:(1 2 3)",
                "qt",
                "/export",
                "fl",
                "intdv",
                "sort",
                "floatdv asc,stringdv asc,intdv desc"));
    assertJsonEquals(
        s,
        "{\"responseHeader\": {\"status\": 0}, \"response\":{\"numFound\":3, \"docs\":[{\"intdv\":3},{\"intdv\":2},{\"intdv\":1}]}}");

    // Test three sort fields
    s =
        h.query(
            req(
                "q",
                "id:(1 2 3)",
                "qt",
                "/export",
                "fl",
                "intdv",
                "sort",
                "floatdv asc,stringdv desc,intdv asc"));
    assertJsonEquals(
        s,
        "{\"responseHeader\": {\"status\": 0}, \"response\":{\"numFound\":3, \"docs\":[{\"intdv\":1},{\"intdv\":2},{\"intdv\":3}]}}");

    // Test four sort fields
    s =
        h.query(
            req(
                "q",
                "id:(1 2 3)",
                "qt",
                "/export",
                "fl",
                "intdv",
                "sort",
                "floatdv asc,floatdv desc,floatdv asc,intdv desc"));
    assertJsonEquals(
        s,
        "{\"responseHeader\": {\"status\": 0}, \"response\":{\"numFound\":3, \"docs\":[{\"intdv\":3},{\"intdv\":2},{\"intdv\":1}]}}");

    // Test five sort fields
    s =
        h.query(
            req(
                "q",
                "id:(1 2 3)",
                "qt",
                "/export",
                "fl",
                "intdv",
                "sort",
                "intdv desc,floatdv asc,floatdv desc,floatdv asc,intdv desc"));
    assertJsonEquals(
        s,
        "{\"responseHeader\": {\"status\": 0}, \"response\":{\"numFound\":3, \"docs\":[{\"intdv\":3},{\"intdv\":2},{\"intdv\":1}]}}");
    s =
        h.query(
            req(
                "q",
                "id:(1 2 3)",
                "qt",
                "/export",
                "fl",
                "intdv",
                "sort",
                "floatdv desc,intdv asc,floatdv desc,floatdv desc,intdv desc"));
    assertJsonEquals(
        s,
        "{\"responseHeader\": {\"status\": 0}, \"response\":{\"numFound\":3, \"docs\":[{\"intdv\":1},{\"intdv\":2},{\"intdv\":3}]}}");

    // Test six sort fields
    s =
        h.query(
            req(
                "q",
                "id:(1 2 3)",
                "qt",
                "/export",
                "fl",
                "intdv",
                "sort",
                "floatdv asc,intdv desc,floatdv asc,floatdv desc,floatdv asc,intdv asc"));
    assertJsonEquals(
        s,
        "{\"responseHeader\": {\"status\": 0}, \"response\":{\"numFound\":3, \"docs\":[{\"intdv\":3},{\"intdv\":2},{\"intdv\":1}]}}");

    // Test seven sort fields
    s =
        h.query(
            req(
                "q",
                "id:(1 2 3)",
                "qt",
                "/export",
                "fl",
                "intdv",
                "sort",
                "floatdv desc,intdv asc,floatdv desc,floatdv asc,floatdv desc,floatdv asc,intdv desc"));
    assertJsonEquals(
        s,
        "{\"responseHeader\": {\"status\": 0}, \"response\":{\"numFound\":3, \"docs\":[{\"intdv\":1},{\"intdv\":2},{\"intdv\":3}]}}");

    // Test eight sort fields
    s =
        h.query(
            req(
                "q",
                "id:(1 2 3)",
                "qt",
                "/export",
                "fl",
                "intdv",
                "sort",
                "floatdv asc,intdv desc,floatdv asc,floatdv desc,floatdv asc,floatdv desc,floatdv asc,intdv asc"));
    assertJsonEquals(
        s,
        "{\"responseHeader\": {\"status\": 0}, \"response\":{\"numFound\":3, \"docs\":[{\"intdv\":3},{\"intdv\":2},{\"intdv\":1}]}}");
    s =
        h.query(
            req(
                "q",
                "id:(1 2 3)",
                "qt",
                "/export",
                "fl",
                "intdv",
                "sort",
                "intdv asc,floatdv desc,floatdv asc,floatdv desc,floatdv asc,floatdv desc,floatdv asc,intdv desc"));
    assertJsonEquals(
        s,
        "{\"responseHeader\": {\"status\": 0}, \"response\":{\"numFound\":3, \"docs\":[{\"intdv\":1},{\"intdv\":2},{\"intdv\":3}]}}");

    // Test nine sort fields
    s =
        h.query(
            req(
                "q",
                "id:(1 2 3)",
                "qt",
                "/export",
                "fl",
                "intdv",
                "sort",
                "intdv asc,floatdv desc,floatdv asc,floatdv desc,floatdv asc,floatdv desc,intdv asc,intdv desc,floatdv asc"));
    assertJsonEquals(
        s,
        "{\"responseHeader\": {\"status\": 0}, \"response\":{\"numFound\":3, \"docs\":[{\"intdv\":1},{\"intdv\":2},{\"intdv\":3}]}}");
    s =
        h.query(
            req(
                "q",
                "id:(1 2 3)",
                "qt",
                "/export",
                "fl",
                "intdv",
                "sort",
                "floatdv asc,intdv desc,floatdv asc,floatdv desc,floatdv asc,floatdv desc,intdv desc,intdv asc,floatdv asc"));
    assertJsonEquals(
        s,
        "{\"responseHeader\": {\"status\": 0}, \"response\":{\"numFound\":3, \"docs\":[{\"intdv\":3},{\"intdv\":2},{\"intdv\":1}]}}");

    // Test ten sort fields
    s =
        h.query(
            req(
                "q",
                "id:(1 2 3)",
                "qt",
                "/export",
                "fl",
                "intdv",
                "sort",
                "intdv asc,floatdv desc,floatdv asc,floatdv desc,floatdv asc,floatdv desc,intdv asc,intdv desc,floatdv desc,floatdv asc"));
    assertJsonEquals(
        s,
        "{\"responseHeader\": {\"status\": 0}, \"response\":{\"numFound\":3, \"docs\":[{\"intdv\":1},{\"intdv\":2},{\"intdv\":3}]}}");
    s =
        h.query(
            req(
                "q",
                "id:(1 2 3)",
                "qt",
                "/export",
                "fl",
                "intdv",
                "sort",
                "floatdv asc,intdv desc,floatdv asc,floatdv desc,floatdv asc,floatdv desc,intdv desc,intdv asc,floatdv desc,floatdv asc"));
    assertJsonEquals(
        s,
        "{\"responseHeader\": {\"status\": 0}, \"response\":{\"numFound\":3, \"docs\":[{\"intdv\":3},{\"intdv\":2},{\"intdv\":1}]}}");

    s = h.query(req("q", "id:(1 2 3)", "qt", "/export", "fl", "intdv", "sort", "doubledv desc"));
    assertJsonEquals(
        s,
        "{\"responseHeader\": {\"status\": 0}, \"response\":{\"numFound\":3, \"docs\":[{\"intdv\":3},{\"intdv\":1},{\"intdv\":2}]}}");

    s =
        h.query(
            req("q", "intdv:[2 TO 1000]", "qt", "/export", "fl", "intdv", "sort", "doubledv desc"));
    assertJsonEquals(
        s,
        "{\"responseHeader\": {\"status\": 0}, \"response\":{\"numFound\":3, \"docs\":[{\"intdv\":3},{\"intdv\":7},{\"intdv\":2}]}}");

    s = h.query(req("q", "stringdv:blah", "qt", "/export", "fl", "intdv", "sort", "doubledv desc"));
    assertJsonEquals(
        s, "{\"responseHeader\": {\"status\": 0}, \"response\":{\"numFound\":0, \"docs\":[]}}");

    s = h.query(req("q", "id:8", "qt", "/export", "fl", "stringdv", "sort", "intdv asc"));
    assertJsonEquals(
        s,
        "{\"responseHeader\": {\"status\": 0}, \"response\":{\"numFound\":1, \"docs\":[{\"stringdv\":\"chello \\\"world\\\"\"}]}}");

    // Test sortable text fields:
    s =
        h.query(
            req(
                "q",
                "id:(1 OR 3 OR 8)",
                "qt",
                "/export",
                "fl",
                "sortabledv_m_udvas,sortabledv_udvas",
                "sort",
                "sortabledv_udvas asc"));
    assertJsonEquals(
        s,
        "{\n"
            + "  \"responseHeader\":{\"status\":0},\n"
            + "  \"response\":{\n"
            + "    \"numFound\":3,\n"
            + "    \"docs\":[{\n"
            + "        \"sortabledv_m_udvas\":[\"this is some text one_1\"\n"
            + "          ,\"this is some text three_1\"\n"
            + "          ,\"this is some text two_1\"]}\n"
            + "      ,{\n"
            + "        \"sortabledv_udvas\":\"this is some text_1\"}\n"
            + "      ,{\n"
            + "        \"sortabledv_m_udvas\":[\"this is some text one_2\"\n"
            + "          ,\"this is some text three_2\"\n"
            + "          ,\"this is some text two_2\"],\n"
            + "        \"sortabledv_udvas\":\"this is some text_2\"}]}}");

    s =
        h.query(
            req(
                "q",
                "id:(1 OR 3 OR 8)",
                "qt",
                "/export",
                "fl",
                "sortabledv_m",
                "sort",
                "sortabledv_udvas asc"));
    assertTrue(
        "Should have 400 status when exporting sortabledv_m, it does not have useDocValuesAsStored='true'",
        s.contains("\"status\":400}"));
    assertTrue(
        "Should have a cause when exporting sortabledv_m, it does not have useDocValuesAsStored='true'",
        s.contains("Must have useDocValuesAsStored='true' to be used with export writer"));

    s =
        h.query(
            req(
                "q",
                "id:(1 OR 3 OR 8)",
                "qt",
                "/export",
                "fl",
                "sortabledv",
                "sort",
                "sortabledv_udvas asc"));
    assertTrue(
        "Should have 400 status when exporting sortabledv, it does not have useDocValuesAsStored='true'",
        s.contains("\"status\":400}"));
    assertTrue(
        "Should have a cause when exporting sortabledv, it does not have useDocValuesAsStored='true'",
        s.contains("Must have useDocValuesAsStored='true' to be used with export writer"));
  }

  private void assertJsonEquals(String actual, String expected) {
    assertEquals(
        Utils.toJSONString(Utils.fromJSONString(expected)),
        Utils.toJSONString(Utils.fromJSONString(actual)));
  }

  private void testExportRequiredParams() throws Exception {

    // Test whether missing required parameters returns expected errors.

    // String s =  h.query(req("q", "id:1", "qt", "/export", "fl",
    // "floatdv,intdv,stringdv,longdv,doubledv", "sort", "intdv asc"));
    String s;
    s = h.query(req("qt", "/export"));
    assertTrue("Should have had a sort error", s.contains("No sort criteria"));
    s = h.query(req("sort", "intdv asc", "qt", "/export"));
    assertTrue("Should have had fl error", s.contains("export field list (fl) must be specified"));
    s = h.query(req("sort", "intdv asc", "qt", "/export", "fl", "stringdv"));
    // Interesting you don't even need to specify a "q" parameter.

  }

  private void testDates() throws Exception {
    String s = h.query(req("q", "id:1", "qt", "/export", "fl", "datedv", "sort", "datedv asc"));
    assertJsonEquals(
        s,
        "{\"responseHeader\": {\"status\": 0}, \"response\":{\"numFound\":1, \"docs\":[{\"datedv\":\"2017-06-16T07:00:00Z\"}]}}");
    s = h.query(req("q", "id:1", "qt", "/export", "fl", "datedv_m", "sort", "datedv asc"));
    assertJsonEquals(
        s,
        "{\"responseHeader\": {\"status\": 0}, \"response\":{\"numFound\":1, \"docs\":[{\"datedv_m\":[\"2017-06-16T01:00:00Z\",\"2017-06-16T02:00:00Z\",\"2017-06-16T03:00:00Z\",\"2017-06-16T04:00:00Z\"]}]}}");
  }

  private void testDuplicates() throws Exception {
    // see SOLR-10924
    String expected =
        h.getCore().getLatestSchema().getField("int_is_t").getType().isPointField()
            ? "1,1,1,1"
            : "1";
    String s = h.query(req("q", "id:3", "qt", "/export", "fl", "int_is_t", "sort", "intdv asc"));
    assertJsonEquals(
        s,
        "{\"responseHeader\": {\"status\": 0}, \"response\":{\"numFound\":1, \"docs\":[{\"int_is_t\":["
            + expected
            + "]}]}}");
    expected =
        h.getCore().getLatestSchema().getField("int_is_p").getType().isPointField()
            ? "1,1,1,1"
            : "1";
    s = h.query(req("q", "id:8", "qt", "/export", "fl", "int_is_p", "sort", "intdv asc"));
    assertJsonEquals(
        s,
        "{\"responseHeader\": {\"status\": 0}, \"response\":{\"numFound\":1, \"docs\":[{\"int_is_p\":[1,1,1,1]}]}}");
  }

  /**
   * This test doesn't validate the correctness of results, it just compares the response of the
   * same request when asking for Trie fields vs Point fields. Can be removed once Trie fields are
   * no longer supported
   */
  @Test
  @SuppressForbidden(reason = "using new Date(time) to create random dates")
  public void testRandomNumerics() throws Exception {
    SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ROOT);
    assertU(delQ("*:*"));
    assertU(commit());
    List<String> trieFields = new ArrayList<String>();
    List<String> pointFields = new ArrayList<String>();
    for (String mv : new String[] {"s", ""}) {
      for (String indexed : new String[] {"_ni", ""}) {
        for (String type : new String[] {"i", "l", "f", "d", "dt"}) {
          String field = "number_" + type + mv + indexed;
          SchemaField sf = h.getCore().getLatestSchema().getField(field + "_t");
          assertTrue(sf.hasDocValues());
          assertNotNull(sf.getType().getNumberType());

          sf = h.getCore().getLatestSchema().getField(field + "_p");
          assertTrue(sf.hasDocValues());
          assertNotNull(sf.getType().getNumberType());
          assertTrue(sf.getType().isPointField());

          trieFields.add(field + "_t");
          pointFields.add(field + "_p");
        }
      }
    }
    for (int i = 0; i < atLeast(100); i++) {
      if (random().nextInt(20) == 0) {
        // have some empty docs
        assertU(adoc("id", String.valueOf(i)));
        continue;
      }

      if (random().nextInt(20) == 0 && i > 0) {
        // delete some docs
        assertU(delI(String.valueOf(i - 1)));
      }

      SolrInputDocument doc = new SolrInputDocument();
      doc.addField("id", String.valueOf(i));
      addInt(doc, random().nextInt(), false);
      addLong(doc, random().nextLong(), false);
      addFloat(doc, random().nextFloat() * 3000 * (random().nextBoolean() ? 1 : -1), false);
      addDouble(doc, random().nextDouble() * 3000 * (random().nextBoolean() ? 1 : -1), false);
      addDate(doc, dateFormat.format(new Date()), false);

      // MV needs to be unique in order to be the same in Trie vs Points
      Set<Integer> ints = new HashSet<>();
      Set<Long> longs = new HashSet<>();
      Set<Float> floats = new HashSet<>();
      Set<Double> doubles = new HashSet<>();
      Set<String> dates = new HashSet<>();
      for (int j = 0; j < random().nextInt(20); j++) {
        ints.add(random().nextInt());
        longs.add(random().nextLong());
        floats.add(random().nextFloat() * 3000 * (random().nextBoolean() ? 1 : -1));
        doubles.add(random().nextDouble() * 3000 * (random().nextBoolean() ? 1 : -1));
        dates.add(dateFormat.format(new Date(System.currentTimeMillis() + random().nextInt())));
      }
      ints.stream().forEach((val) -> addInt(doc, val, true));
      longs.stream().forEach((val) -> addLong(doc, val, true));
      floats.stream().forEach((val) -> addFloat(doc, val, true));
      doubles.stream().forEach((val) -> addDouble(doc, val, true));
      dates.stream().forEach((val) -> addDate(doc, val, true));

      assertU(adoc(doc));
      if (random().nextInt(20) == 0) {
        assertU(commit());
      }
    }
    assertU(commit());
    doTestQuery("id:1", trieFields, pointFields);
    doTestQuery("*:*", trieFields, pointFields);
    // "id" field is really a string, this is not a numeric range query
    doTestQuery("id:[0 TO 2]", trieFields, pointFields);
    doTestQuery("id:[0 TO 9]", trieFields, pointFields);
    doTestQuery("id:DOES_NOT_EXIST", trieFields, pointFields);
  }

  @Test
  public void testMultipleSorts() throws Exception {
    assertU(delQ("*:*"));
    assertU(commit());

    int numDocs = 1000;

    // 10 unique values
    String[] str_vals = new String[10];
    for (int i = 0; i < str_vals.length; i++) {
      str_vals[i] = TestUtil.randomSimpleString(random(), 10);
    }

    float[] float_vals = new float[10];
    float_vals[0] = 0.0f;
    float_vals[1] = +0.0f;
    float_vals[2] = -0.0f;
    float_vals[3] = +0.00001f;
    float_vals[4] = +0.000011f;
    float_vals[5] = Float.MAX_VALUE;
    float_vals[6] = Float.MIN_VALUE;
    float_vals[7] = 1 / 3f; // 0.33333334
    float_vals[8] = 0.3333333f; // max digits before float loses precision
    float_vals[9] = random().nextFloat();

    for (int i = 0; i < numDocs; i++) {
      int number = TestUtil.nextInt(random(), 0, 9);
      assertU(
          adoc(
              "id",
              String.valueOf(i),
              "floatdv",
              String.valueOf(float_vals[number]),
              "intdv",
              String.valueOf(number),
              "stringdv",
              String.valueOf(str_vals[number]),
              "longdv",
              String.valueOf(number),
              "doubledv",
              String.valueOf(number),
              "datedv",
              randomSkewedDate(),
              "booleandv",
              String.valueOf(random().nextBoolean()),
              "field1_s_dv",
              String.valueOf(str_vals[number]),
              "field2_i_p",
              String.valueOf(number),
              "field3_l_p",
              String.valueOf(number)));
      if (numDocs % 300 == 0) {
        assertU(commit());
      }
    }
    assertU(commit());

    validateSort(numDocs);
  }

  private void createLargeIndex() throws Exception {
    int BATCH_SIZE = 5000;
    int NUM_BATCHES = 20;
    SolrInputDocument[] docs = new SolrInputDocument[BATCH_SIZE];
    for (int i = 0; i < NUM_BATCHES; i++) {
      for (int j = 0; j < BATCH_SIZE; j++) {
        docs[j] =
            new SolrInputDocument(
                "id", String.valueOf(i * BATCH_SIZE + j),
                "sortabledv", TestUtil.randomSimpleString(random(), 2, 3),
                "sortabledv_udvas", String.valueOf((i + j) % 101),
                "small_i_p", String.valueOf((i + j) % 37));
      }
      updateJ(jsonAdd(docs), null);
    }
    assertU(commit());
  }

  @SuppressWarnings("unchecked")
  @Test
  public void testExpr() throws Exception {
    assertU(delQ("*:*"));
    assertU(commit());
    createLargeIndex();
    SolrQueryRequest req =
        req(
            "q",
            "*:*",
            "qt",
            "/export",
            "fl",
            "id",
            "sort",
            "id asc",
            "expr",
            "top(n=2,input(),sort=\"id desc\")");
    assertJQ(
        req,
        "response/numFound==100000",
        "response/docs/[0]/id=='99999'",
        "response/docs/[1]/id=='99998'");
    req =
        req(
            "q",
            "*:*",
            "qt",
            "/export",
            "fl",
            "id,sortabledv_udvas",
            "sort",
            "sortabledv_udvas asc",
            "expr",
            "unique(input(),over=\"sortabledv_udvas\")");
    String rsp = h.query(req);
    Map<String, Object> rspMap = mapper.readValue(rsp, HashMap.class);
    List<Map<String, Object>> docs =
        (List<Map<String, Object>>) Utils.getObjectByPath(rspMap, false, "/response/docs");
    assertNotNull("missing document results: " + rspMap, docs);
    assertEquals("wrong number of unique docs", 101, docs.size());
    for (int i = 0; i < 100; i++) {
      boolean found = false;
      String si = String.valueOf(i);
      for (Map<String, Object> doc : docs) {
        if (doc.get("sortabledv_udvas").equals(si)) {
          found = true;
          break;
        }
      }
      assertTrue("missing value " + i + " in results", found);
    }
    req =
        req(
            "q",
            "*:*",
            "qt",
            "/export",
            "fl",
            "id,sortabledv_udvas,small_i_p",
            "sort",
            "sortabledv_udvas asc",
            "expr",
            "rollup(input(),over=\"sortabledv_udvas\", sum(small_i_p),avg(small_i_p),min(small_i_p),count(*))");
    rsp = h.query(req);
    rspMap = mapper.readValue(rsp, HashMap.class);
    docs = (List<Map<String, Object>>) Utils.getObjectByPath(rspMap, false, "/response/docs");
    assertNotNull("missing document results: " + rspMap, docs);
    assertEquals("wrong number of unique docs", 101, docs.size());
    for (Map<String, Object> doc : docs) {
      assertNotNull("missing sum: " + doc, doc.get("sum(small_i_p)"));
      assertEquals(18000.0, ((Number) doc.get("sum(small_i_p)")).doubleValue(), 1000.0);
      assertNotNull("missing avg: " + doc, doc.get("avg(small_i_p)"));
      assertEquals(18.0, ((Number) doc.get("avg(small_i_p)")).doubleValue(), 1.0);
      assertNotNull("missing count: " + doc, doc.get("count(*)"));
      assertEquals(1000.0, ((Number) doc.get("count(*)")).doubleValue(), 100.0);
    }
    // try invalid field types
    req =
        req(
            "q",
            "*:*",
            "qt",
            "/export",
            "fl",
            "id,sortabledv,small_i_p",
            "sort",
            "sortabledv asc",
            "expr",
            "unique(input(),over=\"sortabledv\")");
    rsp = h.query(req);
    rspMap = mapper.readValue(rsp, HashMap.class);
    assertEquals(
        "wrong response status",
        400,
        ((Number) Utils.getObjectByPath(rspMap, false, "/responseHeader/status")).intValue());
    docs = (List<Map<String, Object>>) Utils.getObjectByPath(rspMap, false, "/response/docs");
    assertEquals("wrong number of docs", 1, docs.size());
    Map<String, Object> doc = docs.get(0);
    assertTrue("doc doesn't have exception", doc.containsKey(StreamParams.EXCEPTION));
    assertTrue(
        "wrong exception message",
        doc.get(StreamParams.EXCEPTION)
            .toString()
            .contains("Must have useDocValuesAsStored='true'"));
  }

  @Test
  public void testGlobFields() throws Exception {
    assertU(delQ("*:*"));
    assertU(commit());
    createLargeIndex();
    SolrQueryRequest req =
        req("q", "*:*", "qt", "/export", "fl", "id,*_udvas,*_i_p", "sort", "id asc");
    assertJQ(
        req,
        "response/numFound==100000",
        "response/docs/[0]/id=='0'",
        "response/docs/[1]/id=='1'",
        "response/docs/[0]/sortabledv_udvas=='0'",
        "response/docs/[1]/sortabledv_udvas=='1'",
        "response/docs/[0]/small_i_p==0",
        "response/docs/[1]/small_i_p==1");

    assertU(delQ("*:*"));
    assertU(commit());
    createLargeIndex();
    req = req("q", "*:*", "qt", "/export", "fl", "*", "sort", "id asc");
    assertJQ(
        req,
        "response/numFound==100000",
        "response/docs/[0]/id=='0'",
        "response/docs/[1]/id=='1'",
        "response/docs/[0]/sortabledv_udvas=='0'",
        "response/docs/[1]/sortabledv_udvas=='1'",
        "response/docs/[0]/small_i_p==0",
        "response/docs/[1]/small_i_p==1");

    String jq = JQ(req);
    assertFalse(
        "Fields without docvalues and useDocValuesAsStored should not be returned",
        jq.contains("\"sortabledv\""));
  }

  @SuppressWarnings("rawtypes")
  private void validateSort(int numDocs) throws Exception {
    // 10 fields
    List<String> fieldNames =
        new ArrayList<>(
            Arrays.asList(
                "floatdv",
                "intdv",
                "stringdv",
                "longdv",
                "doubledv",
                "datedv",
                "booleandv",
                "field1_s_dv",
                "field2_i_p",
                "field3_l_p"));

    SortFields[] fieldSorts = new SortFields[TestUtil.nextInt(random(), 1, fieldNames.size())];
    for (int i = 0; i < fieldSorts.length; i++) {
      fieldSorts[i] =
          new SortFields(fieldNames.get(TestUtil.nextInt(random(), 0, fieldNames.size() - 1)));
      fieldNames.remove(fieldSorts[i].getField());
    }
    String[] fieldWithOrderStrs = new String[fieldSorts.length];
    String[] fieldStrs = new String[fieldSorts.length];
    for (int i = 0; i < fieldSorts.length; i++) {
      fieldWithOrderStrs[i] = fieldSorts[i].getFieldWithOrder();
      fieldStrs[i] = fieldSorts[i].getField();
    }

    String sortStr = String.join(",", fieldWithOrderStrs); // sort : field1 asc, field2 desc
    String fieldsStr = String.join(",", fieldStrs); // fl :  field1, field2

    List<?> exportDocs =
        queryJsonReturnDocs(
            req("q", "*:*", "qt", "/export", "fl", "id," + fieldsStr, "sort", sortStr));
    assertEquals(exportDocs.size(), numDocs);

    // equivalent for /select
    List<?> selectDocs =
        queryJsonReturnDocs(
            req(
                "q",
                "*:*",
                "qt",
                "/select",
                "fl",
                "id," + fieldsStr,
                "sort",
                sortStr,
                "rows",
                Integer.toString(numDocs),
                "wt",
                "json"));

    assertEquals(selectDocs.size(), numDocs);

    // check that both /select and /export return docs with the same values for the sorted fields
    for (int i = 0; i < exportDocs.size(); i++) {
      Map exportDoc = (Map) exportDocs.get(i);
      Map selectDoc = (Map) selectDocs.get(i);
      for (SortFields fieldSort : fieldSorts) {
        String field = fieldSort.fieldName;
        assertEquals(
            "doc " + i + " differs for field " + field, exportDoc.get(field), selectDoc.get(field));
      }
    }
  }

  @SuppressWarnings("rawtypes")
  private List<?> queryJsonReturnDocs(SolrQueryRequest exportReq) throws Exception {
    String respStr = h.query(exportReq);
    Map respMap = mapper.readValue(respStr, Map.class);
    return (List) ((Map) respMap.get("response")).get("docs");
  }

  private static class SortFields {
    String fieldName;
    String sortOrder;
    String[] orders = {"asc", "desc"};

    SortFields(String fn) {
      this.fieldName = fn;
      this.sortOrder = orders[random().nextInt(2)];
    }

    public String getFieldWithOrder() {
      return this.fieldName + " " + this.sortOrder;
    }

    public String getField() {
      return this.fieldName;
    }

    public String getSort() {
      return this.sortOrder;
    }
  }

  private void doTestQuery(String query, List<String> trieFields, List<String> pointFields)
      throws Exception {
    String trieFieldsFl = String.join(",", trieFields);
    String pointFieldsFl = String.join(",", pointFields);
    String sort =
        pickRandom(
                    (String) pickRandom(trieFields.toArray()),
                    (String) pickRandom(pointFields.toArray()))
                .replace("s_", "_")
            + pickRandom(" asc", " desc");
    String resultPoints =
        h.query(req("q", query, "qt", "/export", "fl", pointFieldsFl, "sort", sort));
    String resultTries =
        h.query(req("q", query, "qt", "/export", "fl", trieFieldsFl, "sort", sort));
    assertJsonEquals(resultPoints.replace("_p", ""), resultTries.replace("_t", ""));
  }

  private void addFloat(SolrInputDocument doc, float value, boolean mv) {
    addField(doc, "f", String.valueOf(value), mv);
  }

  private void addDouble(SolrInputDocument doc, double value, boolean mv) {
    addField(doc, "d", String.valueOf(value), mv);
  }

  private void addLong(SolrInputDocument doc, long value, boolean mv) {
    addField(doc, "l", String.valueOf(value), mv);
  }

  private void addInt(SolrInputDocument doc, int value, boolean mv) {
    addField(doc, "i", String.valueOf(value), mv);
  }

  private void addDate(SolrInputDocument doc, String value, boolean mv) {
    addField(doc, "dt", value, mv);
  }

  private void addField(SolrInputDocument doc, String type, String value, boolean mv) {
    doc.addField("number_" + type + (mv ? "s" : "") + "_t", value);
    doc.addField("number_" + type + (mv ? "s" : "") + "_p", value);
    doc.addField("number_" + type + (mv ? "s" : "") + "_ni_t", value);
    doc.addField("number_" + type + (mv ? "s" : "") + "_ni_p", value);
  }
}
